#include "common.h"
#include "stdlib.h"
#include "time.h"

#define COL_WIDTH	6
#define	ROW_WIDTH	4
#define	SET_WIDTH	12
#define	TAG_WIDTH	(27 - COL_WIDTH - SET_WIDTH)

typedef union {
	struct {
		uint32_t col	: COL_WIDTH;
		uint32_t set	: SET_WIDTH;
		uint32_t tag	: TAG_WIDTH;
	};
	uint32_t addr;
} cache2_addr;

#define NR_COL (1 << COL_WIDTH)
#define NR_ROW (1 << ROW_WIDTH)
#define NR_SET (1 << SET_WIDTH)
#define NR_TAG (1 << TAG_WIDTH)

typedef struct {
	uint8_t line[NR_COL];
	bool valid;
	bool dirty;
	uint32_t tag;
} cache2_line;

cache2_line cache2[NR_SET][NR_ROW];

void init_cache2() {
	int s, r;
	for (s = 0; s < NR_SET; s ++) {
		for (r = 0; r < NR_ROW; r ++) {
			cache2[s][r].valid = false;
		}
	}
}

uint32_t dram_read(hwaddr_t addr, size_t len);
void dram_write(hwaddr_t addr, size_t len, uint32_t data);

void cache2_add(hwaddr_t addr) {
	cache2_addr temp;
	temp.addr = addr;
	uint32_t set = temp.set;
	hwaddr_t mem_addr = addr & ~(NR_COL - 1);
	srand(time(0));
	int i, row = rand() % NR_ROW;
	bool full = true;
	for (i = 0; i < NR_ROW; i ++) {
		if (cache2[set][i].valid == false) {
			full = false;
			row = i;
			break;
		}
	}
	if (full && cache2[set][row].dirty == true) {
		cache2_addr back;
		back.addr = 0;
		back.tag = cache2[set][row].tag;
		back.set = set;
		for (i = 0; i < NR_COL / 4; i ++) {
			dram_write(back.addr + 4 * i, 4, *(uint32_t*)(cache2[set][row].line + 4 * i));
		}	
	}
	cache2[set][row].valid = true;
	cache2[set][row].dirty = false;
	cache2[set][row].tag = temp.tag;
	for (i = 0; i < NR_COL / 4; i ++) {
		*(uint32_t*)(cache2[set][row].line + 4 * i) = dram_read(mem_addr + 4 * i, 4);
	}
}

uint32_t cache2_read(hwaddr_t addr, size_t len) {
	uint8_t buf[4];
	cache2_addr temp;
	temp.addr = addr;
	uint32_t tag = temp.tag;
	uint32_t set = temp.set;
	uint32_t col = temp.col;
	int i;
	bool hit = false;
	for (i = 0; i < NR_ROW; i ++) {
		if (tag == cache2[set][i].tag && cache2[set][i].valid){
			memcpy(buf, cache2[set][i].line + col, len);
			hit = true;
			break;
		}
	}
	if (hit == false) {
		cache2_add(addr);
		for (i = 0; i < NR_ROW; i ++) {
			if (tag == cache2[set][i].tag && cache2[set][i].valid){
				memcpy(buf, cache2[set][i].line + col, len);
				break;
			}
		}
	}
	cache2_addr end;
	end.addr = addr + len - 1;
	if (end.set != set) {
		uint32_t offset = 4 - (addr & 3);
		uint32_t data = cache2_read((addr & ~3) + 4, len - offset);
		memcpy(buf + offset, (uint8_t*)&data, len - offset);
	}
	uint32_t *value = (void*)buf;
	return *value;
}

void cache2_write(hwaddr_t addr, size_t len, uint32_t data) {
	uint8_t buf[4];
	memcpy(buf, (uint8_t*)&data, len);
	size_t first, second;
	cache2_addr temp, end;
	temp.addr = addr;
	end.addr = addr + len - 1;
	if (end.set != temp.set) {
		first = 4 - (addr & 3);
		second = len - first;
		uint32_t data2 = *(uint32_t*)(buf + first);
		cache2_write(addr, first, data);
		cache2_write(addr + first, second, data2);
		return;
	}
	uint32_t tag = temp.tag;
	uint32_t set = temp.set;
	uint32_t col = temp.col;
	int i;
	bool hit = false;
	for (i = 0; i < NR_ROW; i ++) {
		if (tag == cache2[set][i].tag && cache2[set][i].valid) {
			memcpy(cache2[set][i].line + col, buf, len);
			cache2[set][i].dirty = true;
			hit = true;
			break;
		}
	}
	if (hit == false) {
		dram_write(addr, len, data);
		cache2_add(addr);
	}
}
